package it.tukano.collections;

import it.tukano.fun.Function1;
import it.tukano.fun.OptionalValue;
import it.tukano.fun.Tuple2;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Extension of a map
 * @author pgi
 * @param <A>
 * @param <B>
 */
public class MapExt<A, B> implements Map<A, B> {

    public static interface View<K, V> {
        
        V get(K key);
    }
    
    public static <K extends Enum<K>, V> MapExt<K, V> newEnumMap(Class<K> c) {
        EnumMap<K, V> map = new EnumMap<K, V>(c);
        return new MapExt<K, V>(map);
    }

    public static <K, V> MapExt<K, V> newInstance(Iterable<Tuple2<K, V>> tuples) {
        return CollectionsExt.toMap(tuples);
    }
    
    public static <K, V> MapExt<K, V> newLinkedHashMap() {
        return new MapExt<K, V>(new LinkedHashMap<K, V>());
    }

    public static <K, V> MapExt<K, V> newHashMap() {
        return new MapExt<K, V>(new HashMap<K, V>());
    }
    
    public static <K, V> MapExt<K, V> newInstance(Map<K, V> map) {
        return new MapExt<K, V>(map);
    }

    private final Map<A, B> map;

    /**
     * Instance initializer
     */
    protected MapExt(Map<A, B> map) {
        this.map = map;
    }
    
    public A findKeyOrElse(B value, A defaultResult) {
        for (Map.Entry<A, B> entry : entrySet()) {
            if(entry.getValue().equals(value)) return entry.getKey();
        }
        return defaultResult;
    }

    public <V> View<A, V> view(final Class<V> valueType) {
        return new View<A, V>() {

            public V get(A key) {
                return valueType.cast(MapExt.this.get(key));
            }
        };
    }

    public <V> View<A, V> view(final Function1<B, V> converter) {
        return new View<A, V>() {

            public V get(A key) {
                return converter.apply(MapExt.this.get(key));
            }
        };
    }
    
    public List<B> listValues(A... keys) {
        List<B> result = new LinkedList<B>();
        for (int i = 0; i < keys.length; i++) {
            A key = keys[i];
            B value = get(key);
            if(value != null) result.add(value);
        }
        return result;
    }

    public MapExt<A, B> push(A key, B value) {
        put(key, value);
        return this;
    }

    public int size() {
        return map.size();
    }

    public boolean isEmpty() {
        return map.isEmpty();
    }

    public boolean containsKey(Object key) {
        return map.containsKey(key);
    }

    public boolean containsValue(Object value) {
        return map.containsValue(value);
    }
    
    public OptionalValue<B> getValue(A key) {
        return new OptionalValue<B>(get(key));
    }

    public <C extends B> C getAs(Object key, Class<C> type) {
        return type.cast(get(key));
    }

    public B getOrElse(Object key, B alt) {
        final B value = get(key);
        return value == null ? alt : value;
    }

    public B add(A key, B value) {
        put(key, value);
        return value;
    }

    public B get(Object key) {
        return map.get(key);
    }

    public B put(A key, B value) {
        return map.put(key, value);
    }

    public B remove(Object key) {
        return map.remove(key);
    }

    public void putAll(Map<? extends A, ? extends B> m) {
        map.putAll(m);
    }

    public MapExt<A, B> putAll(Class<A> keyType, Class<B> valueType, Object... keyValuePairs) {
        for (int i = 0; i < keyValuePairs.length; i += 2) {
            put(keyType.cast(keyValuePairs[i]), valueType.cast(keyValuePairs[i + 1]));
        }
        return this;
    }

    public void clear() {
        map.clear();
    }

    public Set<A> keySet() {
        return map.keySet();
    }

    public Collection<B> values() {
        return map.values();
    }

    public Set<Entry<A, B>> entrySet() {
        return map.entrySet();
    }

    public B getOrAdd(A key, B value) {
        B storedValue = get(key);
        if(storedValue == null) put(key, storedValue = value);
        return storedValue;
    }
}
